package com.apobates.jforum2.troll.threads.biz.impl;

import com.apobates.jforum2.troll.attention.core.decorater.ForumEncoder;
import com.apobates.jforum2.troll.regular.*;
import com.apobates.jforum2.troll.threads.biz.TopicService;
import com.apobates.jforum2.troll.threads.biz.dao.TopicCategoryDao;
import com.apobates.jforum2.troll.threads.biz.dao.TopicConfigDao;
import com.apobates.jforum2.troll.threads.biz.dao.TopicDao;
import com.apobates.jforum2.troll.threads.biz.dao.TopicStatsDao;
import com.apobates.jforum2.troll.threads.biz.dao.TopicTagDao;
import com.apobates.jforum2.troll.threads.biz.impl.api.TopicActionLogStreamProvider;
import com.apobates.jforum2.troll.threads.entity.Album;
import com.apobates.jforum2.troll.threads.entity.AlbumPicture;
import com.apobates.jforum2.troll.threads.entity.Topic;
import com.apobates.jforum2.troll.threads.entity.TopicCategory;
import com.apobates.jforum2.troll.threads.entity.TopicConfig;
import com.apobates.jforum2.troll.threads.entity.TopicTag;
import com.apobates.jforum2.troll.threads.plug.AbstractPlugTopic;
import com.apobates.jforum2.troll.threads.plug.PlugTopic;
import com.apobates.jforum2.troll.utils.core.Commons;
import com.apobates.jforum2.troll.utils.core.DateTimeUtils;
import com.apobates.jforum2.troll.utils.core.api.ActionEventCulpritor;
import com.apobates.jforum2.troll.utils.core.api.AnonyActionEventCulpritor;
import com.apobates.jforum2.troll.utils.persistence.Page;
import com.apobates.jforum2.troll.utils.persistence.Pageable;
import com.apobates.jforum2.utils.nlp.TagExtraction;
import com.apobates.jforum2.utils.nlp.impl.TagNLPExtraction;
import com.apobates.jforum2.utils.nlp.impl.WordNatureEnum;
import io.seata.spring.annotation.GlobalTransactional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import com.apobates.jforum2.troll.threads.biz.impl.api.TopicFeignServiceApi;
import com.apobates.jforum2.troll.threads.entity.TagRelateTopic;
import com.apobates.jforum2.utils.nlp.AbstractTagRelateQueryExecutor;
import com.apobates.jforum2.utils.nlp.RelateWordStats;
import com.apobates.jforum2.utils.nlp.TagRelateResult;
import com.apobates.jforum2.utils.nlp.impl.relate.TagRelateDistance;
import java.util.function.BiFunction;
import java.util.function.Function;

/**
 *
 * @author xiaofanku
 * @since 20200514
 */
//@CacheConfig(cacheNames = "topicCache")
@Service
public class TopicServiceImpl implements TopicService{
    @Autowired
    private TopicDao topicDao;
    @Autowired
    private TopicConfigDao topicConfigDao;
    @Autowired
    private TopicStatsDao topicStatsDao;
    @Autowired
    private TopicCategoryDao topicCategoryDao;
    @Resource
    private TopicFeignServiceApi topicFeignServiceApi;
    @Autowired
    private TopicActionLogStreamProvider topicActionLogStreamProvider;
    @Autowired
    private TopicTagDao topicTagDao;
    private final static Logger logger = LoggerFactory.getLogger(TopicServiceImpl.class);

    //@Cacheable(key="#id", unless="#result==null")
    @Override
    public Optional<Topic> get(long id) {
        if(id>0){
            return topicDao.findOne(id);
        }
        return Optional.empty();
    }

    //@CacheEvict(key="#id")
    @ActionDescriptor(action=ForumActionEnum.TOPIC_TOP)
    @Transactional
    @GlobalTransactional
    @Override
    public Optional<Boolean> editTop(long id, ActionEventCulpritor culpritor) { //ASP-T1
        return topicDao.editTop(id, true);
    }
    
    //@CacheEvict(key="#id")
    @Transactional
    @GlobalTransactional
    @Override
    public Optional<Boolean> removeTop(long id, ActionEventCulpritor culpritor) {
        return topicDao.editTop(id, false);
    }

    //@CacheEvict(key="#id")
    @ActionDescriptor(action=ForumActionEnum.TOPIC_BEST)
    @Transactional
    @GlobalTransactional
    @Override
    public Optional<Boolean> editGoods(long id, ActionEventCulpritor culpritor) { //ASP-T2
        return topicDao.editGoods(id, true);
    }
    
    //@CacheEvict(key="#id")
    @Transactional
    @GlobalTransactional
    @Override
    public Optional<Boolean> removeGoods(long id, ActionEventCulpritor culpritor) {
        return topicDao.editGoods(id, false);
    }

    //@CacheEvict(key="#id")
    @ActionDescriptor(action=ForumActionEnum.TOPIC_LOCK)
    @Transactional
    @GlobalTransactional
    @Override
    public Optional<Boolean> lock(long id, ActionEventCulpritor culpritor) { //ASP-T5
        return topicDao.editStatus(id, ForumEntityStatusEnum.LOCKED);
    }

    //@CacheEvict(key="#id")
    @ActionDescriptor(action=ForumActionEnum.TOPIC_UNLOCK)
    @Transactional
    @GlobalTransactional
    @Override
    public Optional<Boolean> releaseLock(long id, ActionEventCulpritor culpritor) { //ASP-T6
        return topicDao.editStatus(id, ForumEntityStatusEnum.ACTIVE);
    }

    //删除话题
    //@CacheEvict(key="#id")
    @ActionDescriptor(action=ForumActionEnum.TOPIC_DEL)
    @Transactional
    @GlobalTransactional
    @Override
    public Optional<Boolean> remove(long id, ActionEventCulpritor culpritor) { //ASP-T7
        return topicDao.editStatus(id, ForumEntityStatusEnum.DELETE);
    }
    
    //@CacheEvict(key="#id")
    @Transactional
    @GlobalTransactional
    @Override
    public Optional<Boolean> edit(long id, String title, String content, String[] keywords, ImageIOMeta imageIO, ActionEventCulpritor culpritor) throws IllegalArgumentException, IllegalStateException {
        Topic topic = get(id).orElseThrow(()->new IllegalArgumentException("指定的话题不存在或暂时无法访问"));
        //无HTML tag,Emoji安全
        final String encodeTitle = new ForumEncoder(title).noneHtmlTag().parseEmoji().getContent(); //ST1
        final ForumEncoder postsEncode = new ForumEncoder(content).encodeUploadImage(imageIO.getImageBucketDomain(), imageIO.getUploadImageDirectName());
        //话题编辑时在有图->无图|无图->有图切换时错误
        //脱离至回复的服务中@20210415
        //回复像册
        Album album = null;
        List<AlbumPicture> aps = postsEncode.extractUploadImageLink().stream().map(imgLinkDigest -> new AlbumPicture(imgLinkDigest.getUrl(), imgLinkDigest.getAnchor(), imgLinkDigest.getRanking())).collect(Collectors.toList());
        if (null != aps && !aps.isEmpty()) { //重新创建像册|重新设置话题关联的像册
            album = Album.createAlbum(aps, topic).orElse(null);
        }
        
        //标签编辑
        Set<TopicTag> tags = buildTopicTag(id, title, content, keywords);//ST4
        return modifyTopic(topic.getId(), encodeTitle, content, album, tags, culpritor);
    }

    //@CacheEvict(key="#id")
    @Transactional
    @GlobalTransactional
    @Override
    public Optional<Boolean> move(long id, long boardId, long targetBoardId, int targetBoardGroupId, ActionEventCulpritor culpritor)throws IllegalArgumentException,IllegalStateException { //ASP-T11
        //跨服务事务@20210413
        topicDao.moveTopic(id, targetBoardId, targetBoardGroupId);
        String postsAffect = topicFeignServiceApi.balanceReplyTopic(targetBoardId, targetBoardGroupId, id).getBody();
        int postsSize = Commons.stringToInteger(postsAffect, 0);
        if(0>=postsSize){
            throw new IllegalStateException("期待更新的回复数量大于0");
        }
        //平衡版块统计中的话题数量
        topicFeignServiceApi.balanceTopicPosts(targetBoardId, boardId, postsSize);
        //TopicMoveEvent
        //内部：{}
        //外部：{话题移动通知}
        //发布事件
        Topic t = topicDao.findOneById(id);
        topicActionLogStreamProvider.moveTopic(t, targetBoardGroupId, culpritor.getMemberId(), culpritor.getMemberNickname());
        return Optional.of(true);
    }

    //@CacheEvict(key="'topic_config_'+#id")
    @ActionDescriptor(action=ForumActionEnum.TOPIC_CONFIG_EDIT)
    @Transactional
    @GlobalTransactional
    @Override
    public Optional<Boolean> editTopicConfig(long id, TopicConfig updateConfig, long configId, ActionEventCulpritor culpritor)throws IllegalArgumentException { //ASP-T12 
        TopicConfig tc = topicConfigDao.findOne(configId).orElseThrow(()->new IllegalArgumentException("话题配置文件不存在"));
        tc.setPrivacy(updateConfig.isPrivacy());
        tc.setReply(updateConfig.isReply());
        tc.setNotify(updateConfig.isNotify());
        tc.setReadMinScore(updateConfig.getReadMinScore());
        tc.setReadLowMemberGroup(updateConfig.getReadLowMemberGroup());
        tc.setReadLowMemberRole(updateConfig.getReadLowMemberRole());
        tc.setReadLowMemberLevel(updateConfig.getReadLowMemberLevel());
        tc.setWriteMinScore(updateConfig.getWriteMinScore());
        tc.setWriteLowMemberGroup(updateConfig.getWriteLowMemberGroup());
        tc.setWriteLowMemberRole(updateConfig.getWriteLowMemberRole());
        tc.setWriteLowMemberLevel(updateConfig.getWriteLowMemberLevel());
        tc.setAtomPoster(updateConfig.isAtomPoster());
        return topicConfigDao.edit(tc);
    }

    @Transactional
    @GlobalTransactional
    @Override
    public Optional<Topic> create(int volumesId, long boardId, String title, String content, ImageIOMeta imageIO, ActionEventCulpritor culpritor) { //ASP-T13
        return create(volumesId, boardId, 0, title, content, imageIO, culpritor);
    }

    //创建话题
    @Transactional
    @GlobalTransactional
    @Override
    public Optional<Topic> create(int volumesId, long boardId, int categoryId, String title, String content, ImageIOMeta imageIO, ActionEventCulpritor culpritor) { //ASP-T14
        logger.error("[threads-api][SRV] start publish topic");
        // 编码回复内容
        final ForumEncoder postsEncode = new ForumEncoder(content).encodeUploadImage(imageIO.getImageBucketDomain(), imageIO.getUploadImageDirectName());
        //-------------------------------------------------------------------------
        // 计算TopicCategory
        TopicCategory tc = TopicCategory.empty();
        if (categoryId > 0) {
            tc = topicCategoryDao.findOne(categoryId).orElse(TopicCategory.empty());
        }
        // 无HTML tag,Emoji安全
        final String encodeTitle = new ForumEncoder(title).noneHtmlTag().parseEmoji().getContent();
        Topic topic = new Topic(encodeTitle, culpritor.getMemberId(), culpritor.getMemberNickname(), volumesId, boardId, culpritor.getIpAddr());
        topic.setTopicCategoryName(tc.getNames());
        topic.setTopicCategoryValue(tc.getValue());
        if(culpritor instanceof AnonyActionEventCulpritor){
            topic.setAnony(((AnonyActionEventCulpritor)culpritor).isAnony());
        }
        topic.setSummary(postsEncode.noneHtmlTag().parseEmoji().getContent(200));
        // 标签(原始数据：title,content)
        Set<TopicTag> tages = buildTopicTag(-2L, title, content); //需要回填topic.id
        // 话题内容
        // 脱离至回复的服务中@20210415
        // 话题像册
        Album album=null;
        List<AlbumPicture> aps = postsEncode.extractUploadImageLink().stream().map(imgLinkDigest -> new AlbumPicture(imgLinkDigest.getUrl(), imgLinkDigest.getAnchor(), imgLinkDigest.getRanking())).collect(Collectors.toList());
        if (null != aps && !aps.isEmpty()) {
            album = Album.createAlbum(aps, topic).orElse(null); //需要回填topic.id
        }
        //TopicPublishEvent
        //内部：{保存话题的标签,创建话题统计文件并更新版块的统计,创建话题配置文件}
        //外部：{}
        // 最后一步
        logger.error("[threads-api][SRV] store topic start");
        Optional<Topic> tr = storeReplicaTopic(
                topic,
                content,
                album,
                tages,
                null,
                culpritor,
                topicActionLogStreamProvider.publishTopic(ForumActionEnum.TOPIC_PUBLISH));
        return tr;
    }

    @Transactional
    @GlobalTransactional
    @Override
    public Optional<Topic> create(int volumesId, long boardId, String categoryName, String categoryValue, String title, String content, String[] keywords, ImageIOMeta imageIO, ActionEventCulpritor culpritor){
        int boardGroupId = (volumesId > 0) ? volumesId : 0;
        // 编码回复内容
        final ForumEncoder postsEncode = new ForumEncoder(content).encodeUploadImage(imageIO.getImageBucketDomain(), imageIO.getUploadImageDirectName());
        // 无HTML tag,Emoji安全
        final String encodeTitle = new ForumEncoder(title).noneHtmlTag().parseEmoji().getContent();
        Topic topic = new Topic(encodeTitle, culpritor.getMemberId(), culpritor.getMemberNickname(), boardGroupId, boardId, culpritor.getIpAddr());
        topic.setTopicCategoryName(categoryName);
        topic.setTopicCategoryValue(categoryValue);
        topic.setSummary(postsEncode.noneHtmlTag().parseEmoji().getContent(200));
        // 标签
        Set<TopicTag> tages = buildTopicTag(-2L, title, content, keywords); //需要回填topic.id
        // 话题内容
        // 脱离至回复的服务中@20210415
        // 话题像册
        Album album = null;
        List<AlbumPicture> aps = postsEncode.extractUploadImageLink().stream().map(imgLinkDigest -> new AlbumPicture(imgLinkDigest.getUrl(), imgLinkDigest.getAnchor(), imgLinkDigest.getRanking())).collect(Collectors.toList());
        if (null != aps && !aps.isEmpty()) {
            album = Album.createAlbum(aps, topic).orElse(null); //需要回填topic.id
        }
        // 最后一步
        Optional<Topic> tr = storeReplicaTopic(
                topic,
                content,
                album,
                tages,
                null,
                culpritor,
                topicActionLogStreamProvider.publishTopic(ForumActionEnum.TOPIC_PUBLISH));
        return tr;
    }

    @Transactional
    @GlobalTransactional
    @Override
    public Optional<Topic> createTermArticle(int sectionId, long termId, String title, String content, ImageIOMeta imageIO, ActionEventCulpritor culpritor) {
        // 编码回复内容
        final ForumEncoder postsEncode = new ForumEncoder(content).encodeUploadImage(imageIO.getImageBucketDomain(), imageIO.getUploadImageDirectName());
        //-------------------------------------------------------------------------
        // 无HTML tag,Emoji安全
        final String encodeTitle = new ForumEncoder(title).noneHtmlTag().parseEmoji().getContent();
        Topic article = Topic.buildArticle(encodeTitle, culpritor.getMemberId(), culpritor.getMemberNickname(), sectionId, termId, culpritor.getIpAddr());
        article.setSummary(postsEncode.noneHtmlTag().parseEmoji().getContent(200));
        // 文章内容
        // 脱离至回复的服务中@20210415
        // 文章像册
        Album album = null;
        List<AlbumPicture> aps = postsEncode.extractUploadImageLink().stream().map(imgLinkDigest -> new AlbumPicture(imgLinkDigest.getUrl(), imgLinkDigest.getAnchor(), imgLinkDigest.getRanking())).collect(Collectors.toList());
        if (null != aps && !aps.isEmpty()) {
            album = Album.createAlbum(aps, article).orElse(null); //需要回填topic.id
        }
        // 文章配置
        TopicConfig config = TopicConfig.originConfig(-1L); //需要回填topic.id
        //PlugTopicPublishEvent
        //内部：{话题的配置文件,话题的动作记录,创建话题统计文件并更新版块的统计}
        //外部：{}
        // 最后一步
        Optional<Topic> tr = storeReplicaTopic(
                article,
                content,
                album,
                null,
                config,
                culpritor,
                topicActionLogStreamProvider.publishTopic(ForumActionEnum.TOPIC_PUBLISH));
        return tr;
    }

    //反馈
    @Transactional
    @GlobalTransactional
    @Override
    public Optional<Topic> plug(AbstractPlugTopic topicBuildPlug) {
        final ActionEventCulpritor culpritor = topicBuildPlug.getCulpritor();
        String topicAuthor = (culpritor.getMemberId() > 0)?culpritor.getMemberNickname():String.format("Guest#%d", Commons.ipHashcode(culpritor.getIpAddr()));
        long topicAuthorId = (culpritor.getMemberId() > 0)?culpritor.getMemberId():0L;
        Topic pt = new Topic(topicBuildPlug.getTitle(),
                topicAuthorId,
                topicAuthor,
                topicBuildPlug.getVolumes(),
                topicBuildPlug.getBoard(),
                culpritor.getIpAddr(),
                topicBuildPlug.getCategory().getNames(),
                topicBuildPlug.getCategory().getValue());
        pt.setSuggest(topicBuildPlug.isSuggest());
        // 话题内容 
        // 脱离至回复的服务中@20210415
        // 话题配置
        TopicConfig config = topicBuildPlug.getConfig(); //需要回填topic.id
        // 最后一步
        Optional<Topic> tr = storeReplicaTopic(
                pt,
                topicBuildPlug.getContent(),
                null,
                null,
                config,
                culpritor,
                topicActionLogStreamProvider.publishTopic(topicBuildPlug.getAction()));
        return tr;
    }

    @Transactional
    @GlobalTransactional
    @Override
    public Optional<Topic> plug(PlugTopic topicBuildPlug, final ForumActionEnum action, final ActionEventCulpritor culpritor) {
        String topicAuthor = (culpritor.getMemberId() > 0)?culpritor.getMemberNickname():String.format("Guest#%d", Commons.ipHashcode(culpritor.getIpAddr()));
        long topicAuthorId = (culpritor.getMemberId() > 0)?culpritor.getMemberId():0L;
        Topic pt = Topic.buildArticle(topicBuildPlug.getTitle(), topicAuthorId, topicAuthor, topicBuildPlug.getVolumes(), topicBuildPlug.getBoard(), culpritor.getIpAddr());
        final ForumEncoder postsEncode = new ForumEncoder(topicBuildPlug.getContent());
        pt.setSummary(postsEncode.noneHtmlTag().parseEmoji().getContent(200));
        pt.setSuggest(topicBuildPlug.isSuggest());
        pt.setTopicCategoryName(topicBuildPlug.getCategory().getNames());
        pt.setTopicCategoryValue(topicBuildPlug.getCategory().getValue());
        // 话题内容 
        // 脱离至回复的服务中@20210415
        // 话题配置
        TopicConfig config = topicBuildPlug.getConfig(); //需要回填topic.id
        // 最后一步
        Optional<Topic> tr = storeReplicaTopic(
                pt,
                topicBuildPlug.getContent(),
                null,
                null,
                config,
                culpritor,
                topicActionLogStreamProvider.publishTopic(action));
        return tr;
    }

    //private BiConsumer<Topic,ActionEventCulpritor> actionConsumer(){
    //    return topicActionLogStreamProvider.action(ForumActionEnum.TOPIC_PUBLISH);
    //}
    /*
    private BiConsumer<Topic,ActionEventCulpritor> actionConsumer(final ForumActionEnum action){
        return (t, culpritor)->{
            topicActionLogStreamProvider.send(new TopicActionLog(action, t.getId(), culpritor.getMemberId(), culpritor));
        };
    }*/

    @Transactional
    @GlobalTransactional
    @Override
    public Optional<Boolean> editTermArticle(long articleId, String title, String content, ImageIOMeta imageIO, ActionEventCulpritor culpritor)throws IllegalArgumentException,IllegalStateException {
        Topic topic = get(articleId).orElseThrow(()->new IllegalArgumentException("指定的文章不存在或暂时无法访问"));
        //
        //编辑标题: 无HTML tag,Emoji安全
        final String encodeTitle = new ForumEncoder(title).noneHtmlTag().parseEmoji().getContent(); //ST1
        final ForumEncoder postsEncode = new ForumEncoder(content).encodeUploadImage(imageIO.getImageBucketDomain(), imageIO.getUploadImageDirectName());
        //编辑话题内容
        //脱离至回复的服务中@20210415
        //回复像册
        Album album = null;
        List<AlbumPicture> aps = postsEncode.extractUploadImageLink().stream().map(imgLinkDigest -> new AlbumPicture(imgLinkDigest.getUrl(), imgLinkDigest.getAnchor(), imgLinkDigest.getRanking())).collect(Collectors.toList());
        if (null != aps && !aps.isEmpty()) { //重新创建像册|重新设置话题关联的像册
            album = Album.createAlbum(aps, topic).orElse(null);
        }
        return modifyTopic(topic.getId(), encodeTitle, content, album, null, culpritor);//("文章编辑失败");
    }
    
    @ActionDescriptor(action=ForumActionEnum.TOPIC_LIKED)
    @Transactional
    @GlobalTransactional
    @Override
    public Optional<Boolean> like(long id, ActionEventCulpritor culpritor)throws IllegalStateException { //ASP-T3 | 20200709
        logger.error("[threads-api][SRV] start liked topic");
        //不能重复点赞
        boolean isLiked = isLiked(id, culpritor.getMemberId());
        if (!isLiked) {
            logger.error("[threads-api][SRV] liked is pass");
            throw new IllegalStateException("已经点过赞了");
        }
        topicStatsDao.plusLikes(id);
        //为下次取消点赞清路
        logger.error("[threads-api][SRV] remove liked_cancel action ");
        boolean r = topicFeignServiceApi.removeActionTopic(id, culpritor.getMemberId(), ForumActionEnum.TOPIC_LIKED_CANCEL);
        return Optional.ofNullable(r);
    }
    
    @ActionDescriptor(action=ForumActionEnum.TOPIC_LIKED_CANCEL)
    @Transactional
    @GlobalTransactional
    @Override
    public Optional<Boolean> removeLike(long id, ActionEventCulpritor culpritor)throws IllegalStateException { //ASP-T4 | 20200709
        logger.error("[threads-api][SRV] start cancel liked topic");
        //不能重复取消赞
        long data = topicFeignServiceApi.getActionTopic(id, culpritor.getMemberId(), ForumActionEnum.TOPIC_LIKED_CANCEL);
               //topicActionCollectionDao.countMemberAction(culpritor.getMemberId(), id, ForumActionEnum.TOPIC_LIKED_CANCEL);
        if (data > 0) {
            logger.error("[threads-api][SRV] cancel liked is pass");
            throw new IllegalStateException("点赞已经取消");
        }
        topicStatsDao.negateLikes(id);
        //为下次点赞清路
        logger.error("[threads-api][SRV] remove liked action ");
        boolean r = topicFeignServiceApi.removeActionTopic(id, culpritor.getMemberId(), ForumActionEnum.TOPIC_LIKED);
        return Optional.ofNullable(r);
    }

    @Transactional
    @GlobalTransactional
    @Override
    public Optional<Boolean> removeTermArticle(long id, long termId, ActionEventCulpritor culpritor) {
        return topicDao.removeTermArticle(id, termId);
    }
    
    @ActionDescriptor(action=ForumActionEnum.TOPIC_FAVORITE)
    @Transactional
    @GlobalTransactional
    @Override
    public Optional<Boolean> favorite(long id, ActionEventCulpritor culpritor)throws IllegalStateException{ //ASP-T8 | 20200709
        logger.error("[threads-api][SRV] start favorited topic");
        //更新话题的收藏统计|重复收藏
        boolean isFavorited = isFavorited(id, culpritor.getMemberId());
        if (!isFavorited) {
            logger.error("[threads-api][SRV] favorited is pass");
            throw new IllegalStateException("收藏记录已经存在");
        }
        topicStatsDao.plusFavorites(id);
        //为下次取消收藏清路
        logger.error("[threads-api][SRV] remove favorited_cancel action ");
        boolean r = topicFeignServiceApi.removeActionTopic(id, culpritor.getMemberId(), ForumActionEnum.TOPIC_FAVORITE_CANCEL);
        return Optional.ofNullable(r);
    }
    
    @ActionDescriptor(action=ForumActionEnum.TOPIC_FAVORITE_CANCEL)
    @Transactional
    @GlobalTransactional
    @Override
    public Optional<Boolean> removeFavorite(long id, ActionEventCulpritor culpritor)throws IllegalStateException { //ASP-T9 | 20200709
        logger.error("[threads-api][SRV] start cancel favorited topic");
        //更新话题的收藏统计|重复收藏
        long data = topicFeignServiceApi.getActionTopic(id, culpritor.getMemberId(), ForumActionEnum.TOPIC_FAVORITE_CANCEL);
               //topicActionCollectionDao.countMemberAction(culpritor.getMemberId(), id, ForumActionEnum.TOPIC_FAVORITE_CANCEL);
        if (data > 0) {
            logger.error("[threads-api][SRV] cancel favorited is pass");
            throw new IllegalStateException("已经取消收藏");
        }
        topicStatsDao.negateFavorites(id);
        //为下次收藏清路
        logger.error("[threads-api][SRV] remove favorited action ");
        boolean r = topicFeignServiceApi.removeActionTopic(id, culpritor.getMemberId(), ForumActionEnum.TOPIC_FAVORITE);
        return Optional.ofNullable(r);
    }
    
    @Override
    public boolean isLiked(long id, long memberId) {
        return topicFeignServiceApi.isLikedTopic(id, memberId); 
    }
    
    @Override
    public boolean isFavorited(long id, long memberId) {
        return topicFeignServiceApi.isFavoritedTopic(id, memberId); 
    }
    
    @ActionDescriptor(action=ForumActionEnum.TOPIC_BROWSE)
    @Override
    public Optional<Boolean> browse(long id, ActionEventCulpritor culpritor) { //ASP-T10
        return topicStatsDao.updateDisplaies(id);
    }

    @Override
    public long countAllForMember(long memberId) {
        return topicDao.findAllByMemberCount(memberId);
    }

    /*改由回复服务实现@20210414
    @Override
    public long countAllForMemberReply(long memberId) {
        return topicDao.findAllByMemberReplyCount(memberId);
    }*/
    
    @Override
    public Page<Topic> getTermArticle(long termId, Pageable pageable) {
        return topicDao.findAllTermArticle(termId, pageable);
    }
    
    @Override
    public Page<Topic> getAllForTag(List<String> tagNames, Pageable pageable) {
        return topicDao.findAllByTag(tagNames, pageable); //DAO会进行检查
    }
    
    @Override
    public Stream<Topic> getRecentForBoard(long boardId, int size) {
        return topicDao.findAllByBoard(boardId, size);
    }
    
    @Override
    public Stream<Topic> getRecentForBoardIgnoreStatus(long boardId, int size) {
        return topicDao.findAllByBoardIgnoreStatus(boardId, size);
    }
    
    @Override
    public Stream<Topic> getRecent(int size) {
        return topicDao.findAllOfRecent(size);
    }
    
    @Override
    public Stream<Topic> getRecentIgnoreCondition(int size) {
        return topicDao.findAllOfRecentIgnoreCondition(size);
    }
    
    @Override
    public Stream<Topic> getGoodsForBoardGroup(int boardGroupId, int size) {
        return topicDao.findAllByBoardGroupOfGoods(boardGroupId, size);
    }
    
    @Override
    public Stream<Topic> getMaxReplyForBoardGroup(int boardGroupId, int size) {
        return topicDao.findAllByBoardGroupByMaxReply(boardGroupId, size).stream();
    }
    
    @Override
    public Stream<Topic> getHot(int size) {
        return topicDao.findAllByMaxReply(size).stream();
    }
    
    @Override
    public Stream<Topic> getRecentReply(int size) {
        return topicDao.findAllByReplyDate(size);
    }
    
    @Override
    public Stream<Topic> getAllByBoard(long boardId, ForumEntityStatusEnum status) {
        return topicDao.findAllByBoard(boardId, status);
    }
    
    @Override
    public Stream<Topic> getRecentTermArticle(int size) {
        return topicDao.findAllOfRecentTermArticle(size);
    }
    
    @Override
    public long getRecentByUnixStampSize(long boardId, int prevUnixStamp) {
        LocalDateTime prevDate = DateTimeUtils.getDateTimeByUnixTimestamp(prevUnixStamp);
        return topicDao.findAllRecentByBoardSize(boardId, prevDate);
    }
    
    @Override
    public Map<Long, Long> statsBoardTopicesForToday() {
        LocalDateTime start = DateTimeUtils.getTodayEarlyMorning();
        return statsBoardTopicesForDate(start, LocalDateTime.now());
    }
    
    @Override
    public Map<Long, Long> statsBoardTopicesForDate(LocalDateTime start, LocalDateTime finish) {
        return topicDao.statsBoardTopicSize(start, finish);
    }
    
    @Override
    public TreeMap<String, Long> groupTopicesForDate(LocalDateTime start, LocalDateTime finish) {
        return topicDao.groupTopicSize(start, finish);
    }
    
    @Override
    public Map<ForumEntityStatusEnum, Long> groupTopicesForStatus() {
        return topicDao.groupTopicesForStatus();
    }
    
    @Override
    public Map<String, Long> groupTopicesForCategory() {
        return topicDao.groupTopicesForCategory();
    }
    
    @Override
    public Map<String, Topic> getPrevNextTermArticle(long termId, long articleId) {
        return getPrevNextTermArticleAsync(termId, articleId);
    }
    /*
    @Override
    public Map<Board, Long> groupTopicesForBoard() {
        return CompletableFuture.supplyAsync(()->topicDao.groupTopicesForBoard())
                .thenComposeAsync((Map<Long, Long> rs) -> 
                        CompletableFuture.supplyAsync(() -> 
                                boardDao.findAllById(rs.keySet()).collect(Collectors.toMap(board->board, board->rs.get(board.getId()))))
                        .completeOnTimeout(Collections.emptyMap(), 1, TimeUnit.SECONDS))
                .orTimeout(1, TimeUnit.SECONDS)
                .join();
    }*/
    
    @Override
    public Map<Long, Long> groupTopicesForBoard() {
        return topicDao.groupTopicesForBoard();
    }


    private Optional<Boolean> modifyTopic(long topicId, String title, String content, Album album, Set<TopicTag> tags, ActionEventCulpritor culpritor)throws IllegalStateException{
        try{
            topicDao.modifyTopic(topicId, title, album, tags);
            topicFeignServiceApi.modifyTopicContent(topicId, content, culpritor.getMemberId(), culpritor.getMemberNickname());
            return Optional.of(true);
        }catch(Exception e){
            if(logger.isDebugEnabled()){
                logger.debug("编辑话题失败", e);
            }
            throw new IllegalStateException("编辑话题失败");
        }
    }

    //从标题和内容中汲取标签[20200109]
    private Set<TopicTag> buildTopicTag(final long topicId, String title, String content, String... keywords) {
        TagExtraction tagExtraction = new TagNLPExtraction(10, WordNatureEnum.Noun, WordNatureEnum.Name, WordNatureEnum.Region, WordNatureEnum.Organization, WordNatureEnum.EFB);
        //构造关建词
        Map<String, Integer> tages = tagExtraction.extract(title, content, keywords);
        if (null == tages || tages.isEmpty()) {
            return Collections.emptySet();
        }
        return tages.entrySet().stream().map(entry->new TopicTag(entry.getKey(), entry.getValue(), topicId)).collect(Collectors.toSet());
    }

    private Optional<Topic> storeReplicaTopic(
            Topic topic,
            String topicContent,
            Album album,
            Set<TopicTag> tages,
            TopicConfig config,
            ActionEventCulpritor culpritor,
            BiConsumer<Topic, ActionEventCulpritor> afterConsumeFun)throws IllegalStateException{
        try{
            logger.error("[topic-TC][publish]store replica topic");
            final long topicId = topicDao.pushTopic(topic, album, config);
            logger.error("[topic-TC][Publish]topic id:"+topicId);
            //话题标签
            //ADD xiaofank@20210919:apo_topic_tag
            if(null!=tages && !tages.isEmpty()) {
                logger.error("[topic-TC][Publish]batch topic tag:"+tages.size());
                Set<TopicTag> tts = tages.stream().peek(tt -> tt.setTopicId(topicId)).collect(Collectors.toSet());
                topicTagDao.batchSave(topicId, tts);
            }
            logger.error("[topic-TC][Publish]entry topic content start");
            topicFeignServiceApi.entryTopicContent(topicId, topicContent, topic.getBoardId(), topic.getVolumesId(), culpritor.getMemberId(), culpritor.getMemberNickname(), culpritor.getToken(), culpritor.getIpAddr());
            logger.error("[topic-TC][Publish]entry topic content finish");
            topic.setId(topicId);
            //版块的话题统计
            logger.error("[topic-TC][Publish]update boardstats recent topic");
            topicFeignServiceApi.updateStatsRecentTopic(topic.getBoardId(), topicId, topic.getTitle(), topic.getMemberId(), topic.getMemberNickname());
            logger.error("[topic-TC][Publish]topic event start");
            if(null!=topic && null!=afterConsumeFun){
                afterConsumeFun.accept(topic, culpritor);
            }
            logger.error("[topic-TC][Publish]topic event finish");
            return Optional.ofNullable(topic);
        }catch(Exception e){
            logger.error("[topic-TC][Publish]exception:"+e.getMessage());
            if(logger.isDebugEnabled()){
                logger.debug("发布话题失败", e);
            }
        }
        throw new IllegalStateException("发布话题失败");
        //return Optional.empty();
    }

    @Override
    public Stream<Topic> getRecent(int boardGroupId, int size) {
        return topicDao.findAllOfRecent(boardGroupId, size);
    }

    @Override
    public Stream<Topic> getTopForBoard(long boardId) {
        return topicDao.findAllByBoardOfTop(boardId);
    }

    @Override
    public Stream<Topic> getRecentByUnixStamp(long boardId, int prevUnixStamp) {
        logger.debug(String.format("[TS]args board:%d, prev ux:%d", boardId, prevUnixStamp));
        LocalDateTime prevDate = DateTimeUtils.getDateTimeByUnixTimestamp(prevUnixStamp);
        logger.debug(String.format("[TS]calc localdate:%s", DateTimeUtils.getRFC3339(prevDate)));
        return topicDao.findAllRecentByBoard(boardId, prevDate);
    }

    @Override
    public Stream<Topic> getRecentRelateContent(int size) {
        return topicDao.findAllOfRecent(size).filter(t -> t.isNormal());
    }

    @Override
    public Stream<Topic> getRecentForMember(long memberId, int size) {
        return topicDao.findAllByMember(memberId, size);
    }

    @Override
    public Stream<Topic> getAllForMemberReply(long memberId, int size) {
        return topicDao.findAllByMemberReply(memberId, size);
    }

    @Override
    public Stream<Topic> getAllForMemberPopular(long memberId, int size) {
        return topicDao.findAllByMemberPopular(memberId, size);
    }

    @Override
    public Page<Topic> getAllForMember(long memberId, Pageable pageable) {
        Page<Topic> rs = topicDao.findAllByMember(memberId, pageable);
        return rs;
    }

    /*
    @Override
    public Page<Topic> getAllForMemberReply(long memberId, Pageable pageable) {
        Page<Topic> rs = topicDao.findAllByMemberReply(memberId, pageable);
        return rs;
    }*/

    @Override
    public Page<Topic> getAll(long boardId, String categoryValue, Pageable pageable) {
        if (!Commons.isNotBlank(categoryValue)) {
            return topicDao.findAllByBoard(boardId, pageable);
        }
        return topicDao.findAllByBoard(boardId, categoryValue, pageable);
    }

    @Override
    public Page<Topic> getAllTopsForBoard(long boardId, Pageable pageable) {
        return topicDao.findAllTopsForBoard(boardId, pageable);
    }

    @Override
    public Page<Topic> getAllGoodsForBoard(long boardId, Pageable pageable) {
        return topicDao.findAllGoodsForBoard(boardId, pageable);
    }

    @Override
    public Page<Topic> getAllHotsForBoard(long boardId, Pageable pageable) {
        return topicDao.findAllHotsForBoard(boardId, pageable);
    }

    
    private Map<String, Topic> getPrevNextTermArticleAsync(long termId, long articleId) {
        CompletableFuture<Optional<Topic>> prev = CompletableFuture.supplyAsync(()->topicDao.findPrevTermArticle(termId, articleId)).completeOnTimeout(Optional.empty(), 1, TimeUnit.SECONDS);
        CompletableFuture<Optional<Topic>> next = CompletableFuture.supplyAsync(()->topicDao.findNextTermArticle(termId, articleId)).completeOnTimeout(Optional.empty(), 1, TimeUnit.SECONDS);
        return prev.thenCombineAsync(next, (prevTopic, nextTopic)->{
            Map<String, Topic> data = new HashMap<>();
            if (prevTopic.isPresent()) {
                data.put("PREV", prevTopic.get());
            }
            if (nextTopic.isPresent()) {
                data.put("NEXT", nextTopic.get());
            }
            return data;
        }).orTimeout(1, TimeUnit.SECONDS).join();
    }
    
    private CompletableFuture<Topic> getTopicAsync(final long id){
        return CompletableFuture.supplyAsync(()->topicDao.findOneById(id));
    }
    /*
    @Override
    public Stream<TopicReplica> getRecentForMember(long memberId, int size) {
        Stream<Topic> data = topicDao.findAllByMember(memberId, size);
        return associatedBoardAsync(data.collect(Collectors.toList()));
    }

    @Override
    public Stream<TopicReplica> getAllForMemberPopular(long memberId, int size) {
        Stream<Topic> data = topicDao.findAllByMemberPopular(memberId, size);
        return associatedBoardAndStatsAsync(data.collect(Collectors.toList()));
    }
    
    @Override
    public List<TopicReplica> getRecentRelateContent(int size) {
        Stream<Topic> data = topicDao.findAllOfRecent(size).filter(t -> t.isNormal());
        return associatedBoardAndStatsAsync(data.collect(Collectors.toList())).collect(Collectors.toList());
    }*/
    
    /*
    @Override
    public Page<TopicReplica> getAll(long boardId, Pageable pageable) {
        Page<Topic> data = topicDao.findAllByBoard(boardId, pageable);
        //
        final long total = data.getTotalElements();
        final Stream<TopicReplica> result = associationTopicStatsAsync(data.getResult().collect(Collectors.toList()));
        //
        return new Page<TopicReplica>() {
            @Override
            public long getTotalElements() {
                return total;
            }
            
            @Override
            public Stream<TopicReplica> getResult() {
                return result;
            }
        };
    }
    
    @Override
    public Page<TopicReplica> getAll(long boardId, String categoryValue, Pageable pageable) { 
        if (!Commons.isNotBlank(categoryValue)) {
            return getAll(boardId, pageable);
        }
        Page<Topic> data = topicDao.findAllByBoard(boardId, categoryValue, pageable);
        //
        final long total = data.getTotalElements();
        final Stream<TopicReplica> result = associationTopicStatsAsync(data.getResult().collect(Collectors.toList()));
        //
        return new Page<TopicReplica>() {
            @Override
            public long getTotalElements() {
                return total;
            }
            
            @Override
            public Stream<TopicReplica> getResult() {
                return result;
            }
        };
    }

    @Override
    public Page<TopicReplica> getAllTopsForBoard(long boardId, Pageable pageable) {
        Page<Topic> data = topicDao.findAllTopsForBoard(boardId, pageable);
        //
        final long total = data.getTotalElements();
        final Stream<TopicReplica> result = associationTopicStatsAsync(data.getResult().collect(Collectors.toList()));
        //
        return new Page<TopicReplica>() {
            @Override
            public long getTotalElements() {
                return total;
            }

            @Override
            public Stream<TopicReplica> getResult() {
                return result;
            }
        };
    }

    @Override
    public Page<TopicReplica> getAllGoodsForBoard(long boardId, Pageable pageable) {
        Page<Topic> data = topicDao.findAllGoodsForBoard(boardId, pageable);
        //
        final long total = data.getTotalElements();
        final Stream<TopicReplica> result = associationTopicStatsAsync(data.getResult().collect(Collectors.toList()));
        //
        return new Page<TopicReplica>() {
            @Override
            public long getTotalElements() {
                return total;
            }

            @Override
            public Stream<TopicReplica> getResult() {
                return result;
            }
        };
    }

    @Override
    public Page<TopicReplica> getAllHotsForBoard(long boardId, Pageable pageable) {
        Page<Topic> data = topicDao.findAllHotsForBoard(boardId, pageable);
        //
        final long total = data.getTotalElements();
        final Stream<TopicReplica> result = associationTopicStatsAsync(data.getResult().collect(Collectors.toList()));
        //
        return new Page<TopicReplica>() {
            @Override
            public long getTotalElements() {
                return total;
            }

            @Override
            public Stream<TopicReplica> getResult() {
                return result;
            }
        };
    }

    @Override
    public Page<TopicReplica> getAllForMember(long memberId, Pageable pageable) {
        Page<Topic> rs = topicDao.findAllByMember(memberId, pageable);
        final Stream<TopicReplica> data = associationTopicStatsAsync(rs.getResult().collect(Collectors.toList()));
        
        return new Page<TopicReplica>() {
            @Override
            public long getTotalElements() {
                return rs.getTotalElements();
            }
            
            @Override
            public Stream<TopicReplica> getResult() {
                return data;
            }
        };
    }
    @Override
    public Page<TopicReplica> getAllForMemberReply(long memberId, Pageable pageable) {
        Page<Topic> rs = topicDao.findAllByMemberReply(memberId, pageable);
        final Stream<TopicReplica> data = associationTopicStatsAsync(rs.getResult().collect(Collectors.toList()));
        return new Page<TopicReplica>() {
            @Override
            public long getTotalElements() {
                return rs.getTotalElements();
            }
            
            @Override
            public Stream<TopicReplica> getResult() {
                return data;
            }
        };
    }
    @Override
    public Page<TopicReplica> getAllRelateContent(LocalDateTime start, LocalDateTime finish, Pageable pageable) {
        Page<Topic> rs = topicDao.findAllByDate(start, finish, pageable);
        
        final Stream<TopicReplica> trs = associatedBoardAndStatsAsync(rs.getResult().collect(Collectors.toList()));
        return new Page<TopicReplica>() {
            @Override
            public long getTotalElements() {
                return rs.getTotalElements();
            }
            
            @Override
            public Stream<TopicReplica> getResult() {
                return trs;
            }
        };
    }
    
    @Override
    public Stream<TopicReplica> getTopForBoard(long boardId) {
        return associationTopicStatsAsync(topicDao.findAllByBoardOfTop(boardId).collect(Collectors.toList()));
    }
    @Override
    public Stream<TopicReplica> getRecent(int boardGroupId, int size) {
        return associationTopicStatsAsync(topicDao.findAllOfRecent(boardGroupId, size).collect(Collectors.toList()));
    }
    */
    
    /*
    @Override
    public List<TopicReplica> getRecentByUnixStamp(long boardId, int prevUnixStamp) {
        logger.info(String.format("[TS]args board:%d, prev ux:%d", boardId, prevUnixStamp));
        LocalDateTime prevDate = DateTimeUtils.getDateTimeByUnixTimestamp(prevUnixStamp);
        logger.info(String.format("[TS]calc localdate:%s", DateTimeUtils.getRFC3339(prevDate)));
        return associationTopicStatsAsync(topicDao.findAllRecentByBoard(boardId, prevDate).collect(Collectors.toList())).collect(Collectors.toList());
    }
    
    @Override
    public Optional<TopicReplica> getTopicContentAndStats(final long id, final ImageIOMeta imageIO){
        CompletableFuture<PostsReplica> oneFloor = CompletableFuture.supplyAsync(()->postsDao.findOneByTopicId(id).map(PostsReplica.copyPosts).orElse(null))
                .thenCompose(postsReplica->
                    CompletableFuture.supplyAsync(()->{
                        ForumPostsDecorator pd = ForumPostsDecorator.init(postsReplica).decorateUploadImageEmptyScale(true).apply(imageIO.getImageBucketDomain(), imageIO.getUploadImageDirectName());
                        postsReplica.setContent(pd.getContent());
                        return postsReplica;
                }).completeOnTimeout(null, 1, TimeUnit.SECONDS));
        return getTopicStatsAsync(id).thenCombine(oneFloor, (topicReplica, postsReplica)->{
            topicReplica.ifPresent(trObj->trObj.setContent(postsReplica));
            return topicReplica;
        }).orTimeout(1, TimeUnit.SECONDS).join();
    }
    
    @Override
    public Optional<TopicReplica> getTopicContentAndStatsForRSS(final long id, final ImageIOMeta imageIO){
        CompletableFuture<PostsReplica> oneFloor = CompletableFuture.supplyAsync(()->postsDao.findOneByTopicId(id).map(PostsReplica.copyPosts).orElse(null))
                .thenCompose(postsReplica->
                    CompletableFuture.supplyAsync(()->{
                        String oneFloorBody = ForumPostsDecorator.init(postsReplica)
                                                .block("作者被禁止发言或内容自动屏蔽")
                                                .decorateUploadImageEmptyScale(false)
                                                .apply(imageIO.getImageBucketDomain(), imageIO.getUploadImageDirectName())
                                                .modify("回复最近由管理人员进行过编辑")
                                                .getContent();
                        postsReplica.setContent(oneFloorBody);
                        return postsReplica;
                    }).completeOnTimeout(null, 1, TimeUnit.SECONDS));
        return getTopicStatsAsync(id).thenCombine(oneFloor, (topicReplica, postsReplica)->{
            topicReplica.ifPresent(trObj->trObj.setContent(postsReplica));
            return topicReplica;
        }).orTimeout(1, TimeUnit.SECONDS).join();
    }
    
    @Override
    public Optional<TopicReplica> getTermArticleContent(final long id, final ImageIOMeta imageIO){
        CompletableFuture<PostsReplica> oneFloor = CompletableFuture.supplyAsync(()->postsDao.findOneByTopicId(id).map(PostsReplica.copyPosts).orElse(null))
                .thenCompose(postsReplica->
                    CompletableFuture.supplyAsync(()->{
                        String oneFloorBody = ForumPostsDecorator.init(postsReplica)
                                                .block("内容自动屏蔽")
                                                .decorateUploadImageEmptyScale(true)
                                                .apply(imageIO.getImageBucketDomain(), imageIO.getUploadImageDirectName())
                                                .getContent();
                        postsReplica.setContent(oneFloorBody);
                        return postsReplica;
                    }).completeOnTimeout(null, 1, TimeUnit.SECONDS));
        return getTopicAsync(id).thenCombine(oneFloor, (topic, postsReplica)->{
            Optional<TopicReplica> tr = Optional.ofNullable(topic).map(TopicReplica.copyTopic);
            tr.ifPresent(trObj->trObj.setContent(postsReplica));
            return tr;
        }).orTimeout(1, TimeUnit.SECONDS).join();
    }
    
    @Override
    public Optional<TopicReplica> getFirstArticleForTerm(long termId, ImageIOMeta imageIO) throws ReplicableException,IllegalArgumentException { //20200709
        Optional<Topic> topic = topicDao.findOneArticleForTerm(termId);
        if(!topic.isPresent()){
            return Optional.empty();
        }
        TopicReplica tr = topic.map(TopicReplica.copyTopic).orElseThrow(()->new ReplicableException("复制话题失败,操作无法继续"));
        //
        PostsReplica oneFloor = postsDao.findOneByTopicId(topic.get().getId()).map(PostsReplica.copyPosts).orElseThrow(()->new IllegalArgumentException("内容不存在或暂时无法访问"));
        //-------------------------------------------------------------------------解码图片
        ForumPostsDecorator pd = ForumPostsDecorator.init(oneFloor).decorateUploadImageEmptyScale(true).apply(imageIO.getImageBucketDomain(), imageIO.getUploadImageDirectName());
        oneFloor.setContent(pd.getContent());
        //-------------------------------------------------------------------------
        tr.setContent(oneFloor);
        return Optional.of(tr);
    }
    
    @Override
    public Optional<TopicReplica> getTopicStats(final long id){
        return getTopicStatsAsync(id).orTimeout(1, TimeUnit.SECONDS).join();
    }
    
    @Override
    public Optional<TopicReplica> get(final long id, final long boardId, final int boardGroupId){
        CompletableFuture<BoardGroup> bg = CompletableFuture.supplyAsync(()->boardGroupDao.findOneById(boardGroupId)).completeOnTimeout(null, 1, TimeUnit.SECONDS);
        return getTopicBoardAsync(id, boardId).thenCombine(bg, (topicReplica, boardGroup)->{
            topicReplica.ifPresent(trObj->trObj.setVolumes(boardGroup));
            return topicReplica;
        }).orTimeout(1, TimeUnit.SECONDS).join();
    }
    
    @Override
    public Optional<TopicReplica> get(final long id, final long boardId){
        return getTopicBoardAsync(id, boardId).orTimeout(1, TimeUnit.SECONDS).join();
    }
    
    @Override
    public Optional<TopicReplica> getTopicConfig(final long id){
        CompletableFuture<TopicConfig> tc = CompletableFuture.supplyAsync(()->topicConfigDao.findOneByTopic(id).orElse(null)).completeOnTimeout(null, 1, TimeUnit.SECONDS);
        return getTopicAsync(id).thenCombineAsync(tc, (topic, topicConfig)->{
            Optional<TopicReplica> tr = Optional.ofNullable(topic).map(TopicReplica.copyTopic);
            tr.ifPresent(trObj->trObj.setConfigure(topicConfig));
            return tr;
        }).orTimeout(1, TimeUnit.SECONDS).join();
    }
    
    @Override
    public Optional<TopicReplica> getTopicContentAndStats(final long id){
        CompletableFuture<TopicStats> ts = CompletableFuture.supplyAsync(()->topicStatsDao.findOneByTopic(id).orElse(null)).completeOnTimeout(null, 1, TimeUnit.SECONDS);
        return getTopicContentAsync(id).thenCombine(ts, (topicReplica, topicStats)->{
            topicReplica.ifPresent(trObj->trObj.setStats(topicStats));
            return topicReplica;
        }).orTimeout(1, TimeUnit.SECONDS).join();
    }

    @Override
    public Optional<TopicReplica> getTopicContentAndStats(long id, ImageIOMeta imageIO, boolean lazyload, String imageScale){
        CompletableFuture<PostsReplica> oneFloor = CompletableFuture.supplyAsync(()->postsDao.findOneByTopicId(id).map(PostsReplica.copyPosts).orElse(null))
                .thenCompose(postsReplica->
                        CompletableFuture.supplyAsync(()->{
                            try {
                                String oneFloorBody = ForumPostsDecorator.init(postsReplica)
                                        .block("内容自动屏蔽")
                                        .decorateUploadImageUseScale(lazyload, imageScale)
                                        .apply(imageIO.getImageBucketDomain(), imageIO.getUploadImageDirectName())
                                        .getContent();
                                postsReplica.setContent(oneFloorBody);
                                return postsReplica;
                            }catch (NullPointerException e){
                                return null;
                            }
                        }).completeOnTimeout(null, 1, TimeUnit.SECONDS));
        CompletableFuture<Optional<TopicReplica>> tc = getTopicAsync(id).thenCombine(oneFloor, (topic, postsReplica)->{
            Optional<TopicReplica> tr = Optional.ofNullable(topic).map(TopicReplica.copyTopic);
            tr.ifPresent(trObj->trObj.setContent(postsReplica));
            return tr;
        });
        CompletableFuture<TopicStats> ts = CompletableFuture.supplyAsync(()->topicStatsDao.findOneByTopic(id).orElse(null)).completeOnTimeout(null, 1, TimeUnit.SECONDS);
        return tc.thenCombine(ts, (optTopicReplica, topicStats)->{
            optTopicReplica.ifPresent(trObj->trObj.setStats(topicStats));
            return optTopicReplica;
        }).orTimeout(1, TimeUnit.SECONDS).join();
    }

    @Override
    public Optional<TopicReplica> getTopicContent(final long id){
        return getTopicContentAsync(id).orTimeout(1, TimeUnit.SECONDS).join();
    }*/
    
    
    @Override
    public Stream<TagRelateTopic> getRelateTopic(long id, int size) { //20200709
        //ST1找到需要计算的集合
        logger.debug("[QR]Step 1 get Topic tages");
        List<TopicTag> tages = topicTagDao.findAllRelateTopic(id, size * 100).collect(Collectors.toList());
        if (tages.isEmpty()) {
            //logger.info("[QR]Step 1.1 Topic tages is empty");
            return Stream.empty();
        }
        Map<Boolean, List<TopicTag>> rs = tages.stream().collect(Collectors.partitioningBy(tt -> tt.getTopicId() == id));//分区:true为话题的标签,false为计算的源
        if (rs.get(false).isEmpty()) {
            logger.debug("[QR]Step 2.1 Tag Relate source is empty");
            return Stream.empty();
        }
        //ST2计算距离.以决定哪个在前哪个在后
        logger.debug("[QR]Step 3 Relate Tag Executor");
        List<RelateWordStats> sourceRS = rs.get(false).parallelStream().map(tt -> new RelateWordStats(tt.getTopicId(), tt.getNames(), tt.getRates())).collect(Collectors.toList());
        AbstractTagRelateQueryExecutor trqe = new TagRelateDistance(rs.get(true).stream().collect(Collectors.toMap(TopicTag::getNames, TopicTag::getRates))).load(sourceRS);
        //ST3排序返回结果
        //logger.info("[QR]Step 4 Relate Topice expect size: " + size);
        Map<Long, TagRelateResult> relateTopices = trqe.getResult(size).collect(Collectors.toMap(TagRelateResult::getTopic, Function.identity()));
        if (relateTopices.isEmpty()) {
            logger.debug("[QR]Step 4.1 Relate Topice is empty");
            return Stream.empty();
        }
        BiFunction<Topic, TagRelateResult, TagRelateTopic> bi = (t, trr)->{
            return new TagRelateTopic(t, trr.getRanking(), trr.getSimilarity());
        };
        return topicDao.findAllById(relateTopices.keySet()).stream().map(topic->bi.apply(topic, relateTopices.get(topic.getId())));
    }
    
    /*

    //[异步]级联话题统计
    private Stream<TopicReplica> associationTopicStatsAsync(final List<Topic> topices) {
        if (null == topices || topices.isEmpty()) {
            return Stream.empty();
        }
        BiFunction<Topic, TopicStats, TopicReplica> bi = (t, ts)->{
            TopicReplica tr = Optional.ofNullable(t).map(TopicReplica.copyTopic).orElseGet(TopicReplica::new);
            if(null != ts){
                tr.setStats(ts);
            }else{
                tr.setStats(TopicStats.empty(t.getId(), t.getMemberId(), t.getMemberNickname()));
            }
            return tr;
        };
        Map<Long, TopicStats> topicStatsMap = CompletableFuture.supplyAsync(()->topices.stream().map(Topic::getId).collect(Collectors.toSet()))
                                .thenCompose(topicIdSet-> CompletableFuture.supplyAsync(()->topicStatsDao.findAllByTopicId(topicIdSet).collect(Collectors.toMap(TopicStats::getTopicId, Function.identity())))
                                                        .completeOnTimeout(Collections.emptyMap(), 1, TimeUnit.SECONDS))
                                .orTimeout(1, TimeUnit.SECONDS)
                                .join();
        Comparator<Topic> comparator = (topic, otherTopic) -> topic.getRankingDateTime().compareTo(otherTopic.getRankingDateTime());
        return topices.parallelStream().sorted(comparator.reversed()).map(topic->bi.apply(topic, topicStatsMap.get(topic.getId())));
    }
    //[异步]级联话题统计 && 所属的版块
    private Stream<TopicReplica> associatedBoardAndStatsAsync(final List<Topic> topices){
        //20200710
        //Key=话题的Id,Value=版块的Id
        final Map<Long, Long> allQueryParam = Commons.collectMap(topices.stream(), Topic::getId, Topic::getBoardId);
        if(null == allQueryParam || allQueryParam.isEmpty()){
            return Stream.empty();
        }
        final TriFunction<Topic, TopicStats, Board, TopicReplica> action = (t, ts, b) ->{
            TopicReplica tr = Optional.ofNullable(t).map(TopicReplica.copyTopic).orElseGet(TopicReplica::new);
            if(null != b){
                tr.copyBoard(b);
            }else{
                tr.copyBoard(Board.empty(t.getBoardId()));
            }
            if(null != ts){
                tr.setStats(ts);
            }else{
                tr.setStats(TopicStats.empty(t.getId(), t.getMemberId(), t.getMemberNickname()));
            }
            return tr;
        };
        CompletableFuture<Map<Long, TopicStats>> topicStats = CompletableFuture.supplyAsync(()->topicStatsDao.findAllByTopicId(allQueryParam.keySet()))
                                                .thenCompose((Stream<TopicStats> ts)->
                                                         CompletableFuture.supplyAsync(()->
                                                                ts.collect(Collectors.toMap(TopicStats::getTopicId, Function.identity()))))
                                                .completeOnTimeout(Collections.emptyMap(), 1, TimeUnit.SECONDS);
        CompletableFuture<Map<Long, Board>> boards = CompletableFuture.supplyAsync(()->boardDao.findAllById(allQueryParam.values()))
                                                .thenCompose((Stream<Board> bs)->
                                                        CompletableFuture.supplyAsync(()->
                                                                bs.collect(Collectors.toMap(Board::getId, Function.identity()))))
                                                .completeOnTimeout(Collections.emptyMap(), 1, TimeUnit.SECONDS);
        return topicStats.thenCombine(boards, (topicStatsMap, boardsMap)->{
            return topices.parallelStream().map(topic->action.apply(topic, topicStatsMap.get(topic.getId()), boardsMap.get(topic.getBoardId())));
        }).orTimeout(1, TimeUnit.SECONDS).join();
    }
    //[异步]级联话题所属的版块
    private Stream<TopicReplica> associatedBoardAsync(final List<Topic> topices){
        if (null == topices || topices.isEmpty()) {
            return Stream.empty();
        }
        BiFunction<Topic, Board, TopicReplica> bi = (t, b)->{
            TopicReplica tr = Optional.ofNullable(t).map(TopicReplica.copyTopic).orElseGet(TopicReplica::new);
            if(null != b){
                tr.copyBoard(b);
            }else{
                tr.copyBoard(Board.empty(t.getBoardId()));
            }
            return tr;
        };
        Map<Long, Board> boards = CompletableFuture.supplyAsync(()->topices.stream().map(Topic::getBoardId).collect(Collectors.toSet()))
                                .thenCompose(boardIdSet-> CompletableFuture.supplyAsync(()->boardDao.findAllById(boardIdSet).collect(Collectors.toMap(Board::getId, Function.identity())))
                                                        .completeOnTimeout(Collections.emptyMap(), 1, TimeUnit.SECONDS))
                                .orTimeout(1, TimeUnit.SECONDS)
                                .join();
        return topices.parallelStream().map(topic->bi.apply(topic, boards.get(topic.getBoardId())));
    }*/
    
    /*
    private CompletableFuture<Optional<TopicReplica>> getTopicStatsAsync(final long id)throws ReplicableException,IllegalArgumentException {
        CompletableFuture<TopicStats> ts = CompletableFuture.supplyAsync(()->topicStatsDao.findOneByTopic(id).orElse(null)).completeOnTimeout(null, 1, TimeUnit.SECONDS);
        return getTopicAsync(id).thenCombineAsync(ts, (topic, topicStats)->{
            Optional<TopicReplica> tr = Optional.ofNullable(topic).map(TopicReplica.copyTopic);
            tr.ifPresent(trObj->trObj.setStats(topicStats));
            return tr;
        });
    }
    private CompletableFuture<Optional<TopicReplica>> getTopicBoardAsync(final long id, final long boardId)throws ReplicableException,IllegalArgumentException {
        CompletableFuture<Board> b = CompletableFuture.supplyAsync(()->boardDao.findOneById(boardId)).completeOnTimeout(null, 1, TimeUnit.SECONDS);
        return getTopicAsync(id).thenCombineAsync(b, (topic, board)->{
            Optional<TopicReplica> tr = Optional.ofNullable(topic).map(TopicReplica.copyTopic);
            tr.ifPresent(trObj->trObj.copyBoard(board));
            return tr;
        });
    }
    private CompletableFuture<Optional<TopicReplica>> getTopicContentAsync(final long id){ 
        CompletableFuture<PostsReplica> pr = CompletableFuture.supplyAsync(()->postsDao.findOneByTopicId(id).map(PostsReplica.copyPosts).orElseGet(PostsReplica::new)).completeOnTimeout(null, 1, TimeUnit.SECONDS);
        return getTopicAsync(id).thenCombineAsync(pr, (topic, postsReplica)->{
            Optional<TopicReplica> tr = Optional.ofNullable(topic).map(TopicReplica.copyTopic);
            tr.ifPresent(trObj->{
                trObj.setContent(postsReplica);
            });
            return tr;
        });
    }*/
}